/*
 *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice,
 *  this list of conditions and the following disclaimer.
 *  Redistributions in binary form must reproduce the above copyright
 *  notice, this list of conditions and the following disclaimer in the
 *  documentation and/or other materials provided with the distribution.
 *  Neither the name of the dreamlu.net developer nor the names of its
 *  contributors may be used to endorse or promote products derived from
 *  this software without specific prior written permission.
 *  Author: Chill 庄骞 (smallchill@163.com)
 */
package org.springblade.modules.oss.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.errors.*;
import org.apache.commons.lang3.StringUtils;
import org.springblade.common.constant.CommonConstant;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.oss.OssTemplate;
import org.springblade.core.oss.model.BladeFile;
import org.springblade.core.secure.utils.SecureUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.desk.entity.Notice;

import org.springblade.modules.oss.builder.oss.OssBuilder;
import org.springblade.modules.oss.endpoint.OssEndpoint;
import org.springblade.modules.oss.entity.Attach;

import org.springblade.modules.oss.entity.BackChunk;
import org.springblade.modules.oss.entity.Oss;
import org.springblade.modules.oss.mapper.AttachMapper;
import org.springblade.modules.oss.mapper.BackChunkMapper;
import org.springblade.modules.oss.mapper.OssMapper;
import org.springblade.modules.oss.service.IAttachService;

import org.springblade.modules.oss.vo.AttachVO;
import org.springblade.modules.oss.vo.CheckChunkVO;
import org.springblade.modules.system.entity.User;
import org.springblade.modules.system.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 附件表 服务实现类
 *
 * @author Chill
 */
@Service
public class AttachServiceImpl extends BaseServiceImpl<AttachMapper, Attach> implements IAttachService, ApplicationRunner {

	@Autowired
	private BackChunkMapper backChunkMapper;


	@Autowired
	private AttachMapper attachMapper;


	private MinioClient minioClient;

	@Autowired
	private OssMapper ossMapper;

	@Autowired
	private IUserService userService;
	/**
	 * 对象存储构建类
	 */
	@Autowired
	private OssBuilder ossBuilder;

	@Lazy
	@Autowired
	private OssEndpoint ossEndpoint;

	/**
	 * 文件合并
	 *
	 * @param targetFile 要形成的文件名
	 * @param folder     要形成的文件夹地址
	 * @param filename   文件的名称
	 */
	public static void merge(String targetFile, String folder, String filename) {
		try {
			Files.createFile(Paths.get(targetFile));
			Files.list(Paths.get(folder))
				.filter(path -> !path.getFileName().toString().equals(filename))
				.sorted((o1, o2) -> {
					String p1 = o1.getFileName().toString();
					String p2 = o2.getFileName().toString();
					int i1 = p1.lastIndexOf("-");
					int i2 = p2.lastIndexOf("-");
					return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
				})
				.forEach(path -> {
					try {
						//以追加的形式写入文件
						Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
						//合并后删除该块
						Files.delete(path);
					} catch (IOException e) {
						System.out.println("e = " + e);
					}
				});
		} catch (IOException e) {
			System.out.println("e = " + e);
		}
	}

	@Override
	public IPage<AttachVO> selectAttachPage(IPage<AttachVO> page, AttachVO attach) {
		return page.setRecords(baseMapper.selectAttachPage(page, attach));
	}

	@Override
	public InputStream download(String url) throws IOException {
		URL urlObject = new URL(url);
		HttpURLConnection connection = (HttpURLConnection) urlObject.openConnection();
		connection.setRequestMethod("POST");
		return connection.getInputStream();
	}

	public void init() {
		Oss oss = ossMapper.selectOne(new LambdaQueryWrapper<Oss>().eq(Oss::getStatus, 2));
		if (oss == null) {
			throw new IllegalStateException("No OSS configuration found.");
		}
		this.minioClient = MinioClient.builder()
			.endpoint(oss.getEndpoint())
			.credentials(oss.getAccessKey(), oss.getSecretKey())
			.build();
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public int postFileUpload(BackChunk chunk, HttpServletResponse response) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
		int result = CommonConstant.UPDATE_FAIL;
		MultipartFile file = chunk.getFile();

		OssTemplate template = ossBuilder.template();
		BladeFile bladeFile = template.putFile(chunk.getFilename(), chunk.getFile());

		chunk.setFilename(bladeFile.getName());
		result = backChunkMapper.insertBackChunk(chunk);
		return result;
	}

	@Override
	public CheckChunkVO getFileUpload(BackChunk chunk, HttpServletResponse response) {
		CheckChunkVO vo = new CheckChunkVO();
		//检查该文件是否存在于filelist中,如果存在,直接返回skipUpload为true,执行闪传
		Attach attach = new Attach();
		attach.setIdentifier(chunk.getIdentifier());
		List<Attach> backFilelists = attachMapper.selectBackFilelistList(attach);
		if (CollectionUtils.isNotEmpty(backFilelists)) {
			response.setStatus(HttpServletResponse.SC_CREATED);
			vo.setSkipUpload(true);
			return vo;
		}

		BackChunk resultChunk = new BackChunk();
		resultChunk.setIdentifier(chunk.getIdentifier());
		List<BackChunk> backChunks = backChunkMapper.selectBackChunkList(resultChunk);
		//将已存在的块的chunkNumber列表返回给前端,前端会规避掉这些块
		if (backChunks != null && !backChunks.isEmpty()) {
			List<Integer> collect = backChunks.stream().map(BackChunk::getChunkNumber).collect(Collectors.toList());
			vo.setUploaded(collect);
		}
		return vo;
	}

	@Override
	public int deleteBackFileByIds(Long id) {
		return 0;
	}

	@Transactional(rollbackFor = Exception.class)
	@Override
	public String mergeFile(Attach fileInfo) {
		Long userId = SecureUtil.getUserId();
		User user = userService.getById(userId);
		//当前文件已存在数据库中时,返回已存在标识
		Attach attach = attachMapper.selectOne(new LambdaQueryWrapper<Attach>().eq(Attach::getIdentifier, fileInfo.getIdentifier()));
		if (attach != null) {
			if (fileInfo.getId() != null) {

				//更新原来的附件信息
				Attach updateAttach = attachMapper.selectById(fileInfo.getId());

				updateAttach.setLink(attach.getLink());
				updateAttach.setDomain(attach.getDomain());
				updateAttach.setName(attach.getName());
				updateAttach.setOriginalName(fileInfo.getOriginalName());
				this.updateById(updateAttach);
				return updateAttach.getLink();
			}
			Attach file = new Attach();
			file.setLink(attach.getLink());
			file.setDomain(attach.getDomain());
			file.setName(attach.getName());
			file.setOriginalName(fileInfo.getOriginalName());
			file.setAttachSize(attach.getAttachSize());

			//设置状态
			file.setStatus(fileInfo.getStatus());
			this.save(file);
			return file.getLink();
		}
		BackChunk resultChunk = new BackChunk();
		resultChunk.setIdentifier(fileInfo.getIdentifier());
		List<BackChunk> backChunks = backChunkMapper.selectBackChunkList(resultChunk);
		Oss oss = ossMapper.selectOne(new LambdaQueryWrapper<Oss>().eq(Oss::getStatus, 2));
		// List to hold all parts
		List<byte[]> allParts = new ArrayList<>();
		for (BackChunk backChunk : backChunks) {
			try {
				// Read part from MinIO
				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				InputStream is = minioClient.getObject(
					GetObjectArgs.builder()
						.bucket(oss.getBucketName())
						.object(backChunk.getFilename())
						.build()
				);

				byte[] buffer = new byte[1024];
				int len;
				while ((len = is.read(buffer)) != -1) {
					baos.write(buffer, 0, len);
				}
				allParts.add(baos.toByteArray());

				is.close();
				baos.close();
			} catch (IOException | MinioException | InvalidKeyException | NoSuchAlgorithmException e) {
				e.printStackTrace();
			}
		}

		int totalSize = allParts.stream().mapToInt(arr -> arr.length).sum();
		byte[] mergedData = new byte[totalSize];
		int offset = 0;
		for (byte[] partData : allParts) {
			System.arraycopy(partData, 0, mergedData, offset, partData.length);
			offset += partData.length;
		}

		OssTemplate template = ossBuilder.template();
		BladeFile bladeFile = template.putFile(fileInfo.getOriginalName(), new ByteArrayInputStream(mergedData));
		//上传之后 需要吧分片进行删除
		List<String> collect = backChunks.stream().map(BackChunk::getFilename).collect(Collectors.toList());
		template.removeFiles(collect);

		fileInfo.setLink(bladeFile.getLink());
		fileInfo.setDomain(bladeFile.getDomain());
		fileInfo.setName(bladeFile.getName());
		boolean save = this.saveOrUpdate(fileInfo);
		if (save) {
			//插入文件记录成功后,删除chunk表中的对应记录,释放空间
			BackChunk backChunk = new BackChunk();
			backChunk.setIdentifier(fileInfo.getIdentifier());
			backChunk.setFilename(fileInfo.getOriginalName());
			backChunkMapper.deleteBackChunkByIdentifier(backChunk);
		}
		return fileInfo.getLink();
	}

	/**
	 * 功能描述:生成块文件所在地址
	 */
	private String generatePath(String uploadFolder, BackChunk chunk) {
		StringBuilder sb = new StringBuilder();
		//文件夹地址/md5
		sb.append(uploadFolder).append("/").append(chunk.getIdentifier());
		//判断uploadFolder/identifier 路径是否存在，不存在则创建
		if (!Files.isWritable(Paths.get(sb.toString()))) {
			try {
				Files.createDirectories(Paths.get(sb.toString()));
			} catch (IOException e) {
				log.error(e.getMessage(), e);
			}
		}
		//文件夹地址/md5/文件名-1
		return sb.append("/")
			.append(chunk.getFilename())
			.append("-")
			.append(chunk.getChunkNumber()).toString();
	}

	@Override
	public void run(ApplicationArguments args) throws Exception {
		init();
	}

	@Override
	public void deleteFile(String ids) {
		List<Attach> attachList = baseMapper.selectBatchIds(Func.toLongList(ids));
		String collect = attachList.stream().map(Attach::getName).collect(Collectors.joining(","));
		this.deleteLogic(Func.toLongList(ids));
		ossEndpoint.removeFiles(collect);
	}

	@Override
	public Boolean deleteAttach(String link) {
		Attach attach = baseMapper.selectOne(new LambdaQueryWrapper<Attach>().like(Attach::getLink, link));
		if (attach != null) {
			ossEndpoint.removeFile(attach.getName());
			baseMapper.deleteById(attach);
		}
		return true;
	}
}
